package cn.demoncat.util.web;

import cn.demoncat.util.exception.TokenRuntimeException;
import cn.demoncat.util.lang.IDUtil;
import cn.demoncat.util.lang.LanguageUtil;
import cn.demoncat.util.lang.LongUtil;
import cn.demoncat.util.lang.OptionalUtil;
import cn.demoncat.util.lang.constant.LongConstant;
import cn.demoncat.util.lang.constant.StringConstant;
import cn.demoncat.util.sec.AESUtil;
import cn.demoncat.util.web.entity.Access;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;

import javax.servlet.http.HttpServletRequest;

/**
 * 用户Token工具类
 *
 * <pre>
 * 用途：生成和校验Token，可用于离线登录认证(jwt，丢失时登录失效)和在线登录认证(jwt+缓存，服务端可以查询和清除token)
 *
 * 与标准jwt对比（如 io.jsonwebtoken/jjwt）：
 * 1、token比较短
 * 2、可以根据token跟踪客户端
 * 3、token中保存userId，可以查询用户信息
 * 4、token对应的用户信息、有效期、有效性等，由服务端保存和控制
 *
 * 请求级别：
 * access = 单次请求
 * token = 单个客户端
 * user = 单个用户
 * </pre>
 *
 * @author 延晓磊
 *
 * @version 2.0
 *
 * @since 2016-9-19
 */
public final class TokenUtil {
 
	/**
	 * Token的通用键名
	 */
	public static final String TOKEN_KEY = "token";

	/**
	 * AccessId的通用键名
	 */
	public static final String ACCESS_KEY = "access";

	/**
	 * AES密钥
	 */
	private static String KEY_CODE = "ASDFGHJKLQWERTYUIOOPZXCVBNM";
	private static String AES_KEY = AESUtil.getKey(KEY_CODE);
	
	/**
	 * 线程访问信息
	 */
	private static final ThreadLocal<Access> THREAD_ACCESS = new ThreadLocal<>();

	/**
	 * 错误消息
	 */
	public static final String MSG_TOKEN_NULL = LanguageUtil.match("Token为空", "Token empty");
	public static final String NSG_TOKEN_ILLEGAL = LanguageUtil.match("Token错误", "Token illegal");
	
	/**
	 * 修改AES密钥
	 *
	 * 1、用户登录时，会附加时间戳，因此即使密钥丢失，被伪造Token，也不会造成影响
	 * 2、如果密钥变更，已签发Token将认证失败，而且需要重新部署所有关联服务，保证密钥一致
	 * 3、如果不能变更本类（TokenUtil）中的AES_KEY，可以在所有相关服务启动前，调用本方法修改密钥（参考 test/test.config.GlobalGeneralConfig）
	 * 4、项目中，可能在其它地方共用本密钥，所以变更时应当留意
	 *
	 * @param keyCode
	 *
	 * @author 延晓磊
	 * @since 2023年02月02日
	 */
	public static void setKeyCode(String keyCode){
		KEY_CODE = keyCode;
		AES_KEY = AESUtil.getKey(keyCode);
	}

	/**
	 * 获取密钥KEY
	 *
	 * @return
	 *
	 * @author 延晓磊
	 * @since 2024年01月10日
	 */
	public static String getKeyCode(){
		return KEY_CODE;
	}

	/**
	 * AES加密
	 *
	 * @param text	源数据
	 * @return 加密数据
	 *
	 * @author 延晓磊
	 *
	 * @since 2016年9月11日
	 */
	public static String encode(String text) {
		return AESUtil.encode(text, AES_KEY);
	}

	/**
	 * AES解密
	 *
	 * @param text	加密数据
	 * @return 源数据
	 *
	 * @author 延晓磊
	 *
	 * @since 2016年9月11日
	 */
	public static String decode(String text) {
		return AESUtil.decode(text, AES_KEY);
	}

	/**
	 * 从线程局部变量获取访问信息
	 * 
	 * 1、局部变量在拦截器中通过getAccess(HttpServletRequest,appId)设置
	 * 2、如果不存在，会重新创建，但appId为空，主要用于没有拦截器的WEB项目中容错处理
	 * 3、异步线程中获取不到Access信息（返回空Access）
	 * 
	 * @return
	 * 
	 * @author 延晓磊
	 *
	 * @since 2018年12月24日
	 */
	public static Access getAccess() {
		Access access = THREAD_ACCESS.get();
		if (access == null) {
			// 容错新建
			access = getAccess(ServletUtil.getRequest(), StringConstant.EMPTY);
		}
		return access;
	}

	/**
	 * 设置Access
	 *
	 * @param access
	 *
	 * @author 延晓磊
	 * @since 2022年08月18日
	 */
	public static Access setAccess(Access access){
		THREAD_ACCESS.set(access);
		return access;
	}
	
	/**
	 * 清除访问信息
	 *
	 * 注：因为SpringMvc在放线程入池时没有清除ThreadLocal的操作，所以多次请求可能拥有相同的ThreadLocal变量，所以在请求完成后要进行清除
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年1月23日
	 */
	public static void clearAccess() {
		THREAD_ACCESS.remove();
	}
	
	/**
	 * 更新访问信息(登录后调用)
	 * 
	 * @param token		绑定用户的新token
	 * @param userId	绑定的用户
	 *
	 * @author 延晓磊
	 *
	 * @since 2019年3月29日
	 */
	public static void updAccess(String token, Long userId) {
		Access access = getAccess();
		access.setUserId(userId);
		access.setToken(token);
	}

	/**
	 * 更新访问信息(登录后调用)
	 *
	 * @param token		绑定用户的新token
	 * @param userId	绑定的用户
	 * @param extend	绑定的扩展数据
	 *
	 * @author 延晓磊
	 *
	 * @since 2019年3月29日
	 */
	public static void updAccess(String token, Long userId, String extend) {
		Access access = getAccess();
		access.setUserId(userId);
		access.setToken(token);
		access.setTokenExtend(extend);
	}
	
	/**
	 * 获取访问信息，设置到线程局部变量（在拦截器中调用）
	 *
	 * 注：防止clearAccess未执行，拦截器前置处理时应调用本方法来重置信息
	 * 
	 * @param request
	 * @param appId		当前服务的编号
	 * @return
	 * 
	 * @author 延晓磊
	 *
	 * @since 2018年12月24日
	 */
	public static Access getAccess(HttpServletRequest request, String appId) {
		Access access = new Access();
		access.setAppId(appId);
		if (request == null) {
			// 非请求
			access.setId(IDUtil.getSnowflakeId());
		}else{
			// 请求ID
			access.setId(StringUtils.defaultIfBlank(TokenUtil.getAccessId(request), IDUtil.getSnowflakeId()));
			// 请求信息
			access.setRequest(request);
			// 用户信息
			access.setToken(TokenUtil.getToken(request));
			if (StringUtils.isNotBlank(access.getToken())) {
				// 解析Token
				TokenInfo tokenInfo = TokenUtil.getTokenInfo(access.getToken());
				if (tokenInfo == null) {
					// 不合法的Token/其此场景的Token
					access.setToken(null);
				}else{
					access.setCtoken(tokenInfo.getCtoken());
					access.setUserId(tokenInfo.getUserId());
					access.setTokenExtend(tokenInfo.getExtend());
				}
			}
			// 代理信息
			if (!access.getIsRest()) {
				access.setReferer(WebUtil.getReferer(request));
				access.setAgent(UserAgentUtil.getAgent(request));
			}
		}
		// 设置局部变量
		return setAccess(access);
	}

	/**
	 * 通过Request获取access.id
	 *
	 * 场景：服务调用时，传递access.id，方便分布式日志跟踪、事务管理等
	 *
	 * @param request
	 * @return
	 *
	 * @author 延晓磊
	 * @since 2022年08月18日
	 */
	public static String getAccessId(HttpServletRequest request){
		//常规请求，通过headers获取
		String access = request.getHeader(ACCESS_KEY);
		//特殊请求（测试）
		if(StringUtils.isBlank(access)){
			access = request.getParameter(ACCESS_KEY);
		}
		return access;
	}

	/**
	 * 通过Access获取UserId
	 * 
	 * @return
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年10月28日
	 */
	public static Long getUserId() {
		return getAccess().getUserId();
	}

	/**
	 * 获取UserId参数
	 *
	 * @param id
	 *
	 * @return null=null,-1=我,x=x
	 *
	 * @author 延晓磊
	 * @since 2020年5月18日
	 */
	public static Long getUserIdParam(Long id) {
		return getUserIdParam(id, getUserId());
	}

	/**
	 * 获取UserId参数
	 *
	 * @param id
	 * @param defaultValue
	 *
	 * @return null=null,-1=Default,x=x
	 *
	 * @author 延晓磊
	 * @since 2020年5月18日
	 */
	public static Long getUserIdParam(Long id, Long defaultValue) {
		return LongConstant.N_1.equals(id) ? defaultValue : id;
	}
	
	/**
	 * 通过Access获取Token
	 * 
	 * @return
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年10月28日
	 */
	public static String getToken() {
		return getAccess().getToken();
	}
	
	/**
	 * 通过Access获取原生Token
	 * 
	 * @return
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年10月28日
	 */
	public static String getCToken() {
		return getAccess().getCtoken();
	}
	
	/**
	 * 从Request-Headers中获取用户的Token
	 * 
	 * 1、因为每次请求都要传递token，拼接在URL中不美观，所以附带在Headers中
	 * 2、通用获取方式：TokenUtil.getToken(request)
	 * 3、Controller注解参数绑定方式：(@RequestHeader String token)
	 *
	 * @param request
	 * @return
	 * 
	 * @author 延晓磊
	 *
	 * @since 2017年7月11日
	 */
	public static String getToken(HttpServletRequest request){
		//常规请求，通过headers获取Token
		String token = request.getHeader(TOKEN_KEY);
		//特殊请求(src,第三方插件请求)时，通过查询参数获取Token
		if(StringUtils.isBlank(token)){
			token = request.getParameter(TOKEN_KEY);
		}
		return token;
	}
	
	/**
	 * 生成原生Token
	 *
	 * @return 时间戳+3位随机数 -> 36进制 -> 大写 (10位，2086年后11位)
	 * 
	 * @author 延晓磊
	 *
	 * @since 2017年4月26日
	 */
	public static String initToken(){
		return IDUtil.getTime36();
	}
	
	/**
	 * 绑定Token和UserId
	 *
	 * @param token		Token
	 * @param userId	用户ID
	 * @return ctoken-userId-currTime
	 * 
	 * @author 延晓磊
	 *
	 * @since 2017年4月26日
	 */
	public static TokenInfo bindToken(String token, Long userId){
		return TokenInfo.build(token, userId, null);
	}

	/**
	 * 绑定Token和UserId
	 *
	 * @param token		Token
	 * @param userId	用户ID
	 * @param extend 	扩展信息
	 * @return ctoken-userId-currTime-extend
	 *
	 * @author 延晓磊
	 *
	 * @since 2017年4月26日
	 */
	public static TokenInfo bindToken(String token, Long userId, String extend){
		return TokenInfo.build(token, userId, extend);
	}

	/**
	 * 获取Token信息
	 *
	 * @param token
	 *
	 * @return token,ctoken,userId,expend,initTime,bindTime
	 *
	 * @author 延晓磊
	 *
	 * @since 2017年4月26日
	 */
	public static TokenInfo getTokenInfo(String token){
		return TokenInfo.build(token);
	}

	/**
	 * 从token中获取原生token部分
	 *
	 * @param token
	 * @return
	 * 
	 * @author 延晓磊
	 *
	 * @since 2017年4月26日
	 */
	public static String getClientToken(String token){
		return OptionalUtil.get(TokenUtil.getTokenInfo(token), TokenInfo::getCtoken);
	}
	 
	/**
	 * 获取token绑定的UserId
	 *
	 * @param token
	 * @return
	 * 
	 * @author 延晓磊
	 *
	 * @since 2017年4月26日
	 */
	public static Long getTokenUser(String token){
		return OptionalUtil.get(TokenUtil.getTokenInfo(token), TokenInfo::getUserId);
	}



	/**
	 * Token信息
	 *
	 * 1、token 为空：token 为空
	 * 2、ctoken为空：token 无效
	 * 3、userId为空：未绑定用户（是否登录，需要根据业务进一步判断）
	 *
	 * @author 延晓磊
	 * @since 2023年06月28日
	 */
	@Data
	public static class TokenInfo{

		// 登录Token：AES(原生Token - 36(用户ID) - 36(绑定时间) - 扩展信息)
		private String token;
		// 原生Token：生成时间 + 3位随机数 > 36进制
		private String ctoken;
		// 用户ID
		private Long userId;
		// 扩展信息
		private String extend;
		// 初始时间：生成clientToken的时间
		private Long initTime;
		// 登录时间：绑定clientToken和userId的时间
		private Long bindTime;

		/**
		 * 设置ctoken
		 *
		 * 1、如果ctoken非法，则赋null
		 * 2、如果ctoken合法，同时赋值initTime
		 *
		 * @return 是否赋值
		 */
		public boolean setCtoken(String ctoken){
			this.initTime = IDUtil.checkTime36(ctoken);
			if (this.initTime == null) {
				// 原生Token无法转时间戳 == 非法Token
				this.ctoken = null;
				return false;
			}else{
				// 成功赋值
				this.ctoken = ctoken;
				return true;
			}
		}

		/**
		 * 是否绑定用户
		 */
		public boolean isBind(){
			return !LongUtil.isEmpty(this.userId);
		}

		/**
		 * 获取Token信息（解析）
		 *
		 * @param token
		 *
		 * @return 解析token并获取Token信息
		 */
		public static TokenInfo build(String token){
			TokenInfo info = new TokenInfo();
			if (token.length() > 20) {
				// 登录Token
				info.setToken(token);
				// 解密
				try {
					token = decode(token);
				}catch (Exception e){
				   	// 不合法的Token/其它场景的Token
					return null;
				}
				// 截取
				String[] split = StringUtils.split(token, StringConstant.BAR, 4);
				// 原生Token
				if (info.setCtoken(split[0])) {
					// 用户ID
					info.setUserId(LongUtil.parse36(split[1]));
					if (split.length > 2) {
						// 登录时间
						info.setBindTime(LongUtil.parse36(split[2]));
						// 扩展信息
						if (split.length == 4) {
							info.setExtend(split[3]);
						}
					}
				}
			}else{
				// 原生Token
				info.setCtoken(token);
			}
			return info;
		}

		/**
		 * 获取Token信息（绑定）
		 *
		 * @param token		Token
		 * @param userId	用户ID
		 * @param extend	扩展信息
		 *
		 * @return 绑定token和userId并获取Token信息
		 */
		public static TokenInfo build(String token, Long userId, String extend){
			// 获取原生Token
			if (token.length() > 20) {
				// 解密 > 截取
				token = StringUtils.split(decode(token), StringConstant.BAR, 2)[0];
			}
			TokenInfo info = new TokenInfo();
			if (!info.setCtoken(token)) {
				throw TokenRuntimeException.ILLEGAL;
			}
			// 绑定userId
			info.setUserId(userId);
			info.setBindTime(System.currentTimeMillis());
			info.setExtend(extend);
			// 拼接 > 加密
			token += StringConstant.BAR + LongUtil.to36(userId) + StringConstant.BAR + LongUtil.to36(info.getBindTime());
			if (StringUtils.isNotBlank(extend)) {
				token += StringConstant.BAR + extend;
			}
			info.setToken(encode(token));
			return info;
		}
	}


}
